package com.jmxcfc.blfsc.ssq.service;

import com.jmxcfc.blfsc.ssq.common.Constants;
import com.jmxcfc.blfsc.ssq.exception.SsqSecurityException;
import com.jmxcfc.blfsc.ssq.util.Md5Util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;


/**
 * 签名工具类
 * @author zhuyiping
 */
@Slf4j
@Service
public class SignatureService {

    //上上签developerId
    @Value("${com.ssq.developerId}")
    private String developerId;
    /**
     * 数字签名算法
     */
    private static final String SHA1_RSA = "SHA1withRSA";



    private static String urlSignParams = "?developerId=%s&rtick=%s&signType=rsa&sign=%s";

    /**
     * 私钥：签名、解密
     */
    private PrivateKey privateKey;

    /**
     * 公钥:验签、加密
     */
    //private PublicKey publicKey;



    /**
     * 私钥(JMX方生成):
     */
    @Value("${com.ssq.privateKeyPath}")
    private String privateKeyFilePath;



    @PostConstruct
    public void init() {
        log.info("初始化上上签私钥--开始");
        initProvider();
        initPrivateKey();
        //initPublicKey();
        log.info("初始化上上签私钥--结束");
    }

    /**
     * 注册Provider
     */
    private void initProvider() {
        try {
            Security.addProvider(new BouncyCastleProvider());
        } catch (Exception e) {
            throw new SsqSecurityException(e.getMessage(), e);
        }
    }

    /**
     * 初始化PrivateKey(应用私钥,JMX方生成),用于加签
     */
    private void initPrivateKey() {
        File privateKeyFile = new File(privateKeyFilePath);
        if (!privateKeyFile.exists()) {
            log.error("上上签初始化私钥:私钥文件不存");
            throw new SsqSecurityException("上上签初始化私钥:私钥文件不存在");
        }
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream((int) privateKeyFile.length());
             BufferedInputStream in = new BufferedInputStream(new FileInputStream(privateKeyFile))) {
            int bufSize = 1024;
            byte[] buffer = new byte[bufSize];
            int len = 0;
            while (-1 != (len = in.read(buffer, 0, bufSize))) {
                bos.write(buffer, 0, len);
            }
            byte[] encodedKey = bos.toByteArray();
            if (encodedKey.length <= 0) {
                log.error("上上签初始化私钥:获取私钥失败");
                throw new SsqSecurityException("上上签初始化私钥:获取私钥失败");
            }
            this.privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(encodedKey)));
            if (this.privateKey == null) {
                throw new SsqSecurityException("上上签初始化私钥:获取私钥失败");
            }
            log.info("上上签初始化私钥成功");
        } catch (Exception e) {
            throw new SsqSecurityException(e.getMessage(), e);
        }
    }


    /**
     * urlParams
     * <p>
     * 对数据 进行私钥进行RSA加密 后进行 Base64 编码
     * POST方法原文字符串示例：
     * 1、先把需要签名计算的参数进行字典序排序，得到一个字符串：developerId=aaaaaartick=14840209310001signType=rsa
     * 2、把请求path附加到字符串后面:developerId=aaaaaartick=14840209310001signType=rsa/openapi/v2/user/reg/
     * 3、计算request body的md5值(假设我们的request body的md5值是fb5939fd51b1a929a6e2b75b187827f7)，附加到字符串后面，得到最终的签名原文字符串：
     * developerId=aaaaaartick=14840209310001signType=rsa/openapi/v2/user/reg/fb5939fd51b1a929a6e2b75b187827f7
     * <p>
     * GET方法原文字符串示例：
     * 1、先把需要签名计算的参数进行字典序排序，得到一个字符串：
     * account=136developerId=1709264203795136512imageName=rtick=14928387557256980signType=rsa
     * 2、把请求path附加到字符串后面，得到签名原文字符串:
     * account=136developerId=1709264203795136512imageName=rtick=14928387557256980signType=rsa/openapi/v2/signatureImage/user/download/
     * rsa之后需要进行一次base64Encode，之后再使用UTF-8进行一次urlEncode（因为base64Encode之后的字符串可能会带有“/”等特殊符号），得到的字符串才能用于http请求的url参数。
     * <p>
     * String requestUrl = host + method + urlParams;
     *
     * @param urlParams   url参数(格式 key=value&key=value)
     *                    (除了默认developerId、rtick、signType三个外) 备注:合同下载时是get请求需要传参数
     * @param requestBody 请求体(post的时候不为空)
     * @return
     */
    public String buildUrlPathParams(String requestUrl, String urlParams, String requestBody) {
        try {
            //时间戳
            String timestamp = getTimestamp();
            //map进行字母升序排序后转key1=value1&key2=value2..模式
            StringBuilder signDataSb = urlParamMapSortAfterToString(timestamp, urlParams);
            //获取requestPath(把请求path附加到字符串后面)
            String requestPath = getRequestPath(requestUrl);
            //因为路径末尾没有斜杠,所以需要拼接 "/"
            //signDataSb.append(requestPath).append("/");
            signDataSb.append(requestPath);
            //计算request body的md5值,并追加在signDataSb后面
            if (StringUtils.isNotBlank(requestBody)) {
                String requestMd5 = Md5Util.md5Digest(requestBody);
                signDataSb .append(requestMd5);
            }
            //私钥进行RSA签名 后进行 Base64 编码
            String rsaSign = signByRSA(signDataSb.toString().getBytes(StandardCharsets.UTF_8));
            log.info("RSA签名结果:{}", rsaSign);
            //rsa算出来的sign，需要urlencode
            rsaSign = URLEncoder.encode(rsaSign,StandardCharsets.UTF_8.name());

            // 签名参数追加为url参数
            return StringUtils.isBlank(urlParams)?String.format(urlSignParams, this.developerId,
                    timestamp, rsaSign):String.format(urlSignParams, this.developerId,
                    timestamp, rsaSign)+ "&" + urlParams;
        } catch (Exception e) {
            log.error("加签失败,失败原因:{}", e.getMessage(), e);
            throw new SsqSecurityException(e.getMessage(), e);
        }
    }


    /**
     * 默认需要签名的参数：
     * developerId、rtick(时间戳)、signType加强类型
     * 1、按照第一个字符的键值 ASCII 码递增排序(字母升序排序)后
     * 2、按照将排序后的参数与其对应值，组合成 参数=参数值 的格式【 key1=value1key2=value2】
     *
     * @param timestamp 时间戳参数
     * @param urlParams url参数(格式 key=value&key=value)
     * @return
     */
    private StringBuilder urlParamMapSortAfterToString(String timestamp, String urlParams) {

        //TreeMap对key进行升序排序
        Map<String, String> signeURLParams = new TreeMap<>();
        signeURLParams.put("developerId", developerId);
        //时间戳
        signeURLParams.put("rtick", timestamp);
        signeURLParams.put("signType", "rsa");
        //如果不为空(get请求的时候传url参数)
        if (StringUtils.isNotBlank(urlParams)) {
            signeURLParams.putAll(parseUrlParams(urlParams));
        }
        StringBuilder signedURLParamsSb = new StringBuilder();
        //TreeMap对key进行升序排序
        signeURLParams.entrySet().forEach(entry -> {
            // key1=value1key2=value2
            if (StringUtils.isNoneBlank(entry.getKey(), entry.getValue())) {
                signedURLParamsSb.append(entry.getKey()).append(Constants.EQUAL).append(entry.getValue().trim());
            }
        });
        return signedURLParamsSb;
    }


    /**
     * 解析urlParams参数
     * url参数(格式 key=value&key=value)
     *
     * @param urlParams
     * @return
     */
    private Map<String, String> parseUrlParams(String urlParams) {
        Map<String, String> dataTreeMap = new TreeMap<>();
        String[] listObject = urlParams.split("&");
        Arrays.stream(listObject).forEach(data -> {
            if (data.matches("(.+?)=(.+)")) {
                String[] tempListObj = data.split("=");
                dataTreeMap.put(tempListObj[0], tempListObj[1]);
            } else if (data.matches("(.+)=")) {
                String[] tempListObj = data.split("=");
                dataTreeMap.put(tempListObj[0], "");
            }
        });
        return dataTreeMap;
    }

    /**
     * url案例:http://10.30.4.15:8081/openapi/v2/user/reg/
     * 获取 /openapi/v2/user/reg/字符串
     * @param requestUrl
     * @return
     */
    private String getRequestPath(String requestUrl) throws Exception {
        URL url = new URL(requestUrl);
        return url.getPath();
    }

    /**
     * 私钥进行RSA签名 后进行 Base64 编码
     * 用私钥对信息生成数字签名
     * @param data  加密数据
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws SignatureException
     */
    private String signByRSA(byte[] data ) throws Exception {
        Signature signature = Signature.getInstance(SHA1_RSA);
        signature.initSign(privateKey);
        //UTF_8
        signature.update(data);
        byte[] signed = signature.sign();
        return new String(Base64.encodeBase64(signed));
    }


    /**
     * 获取时间戳
     * @return
     */
    private String getTimestamp() {
        long timestamp = new Date().getTime();
        int rnd = (int) Math.random() * 1000;
        String rtick = timestamp + "" + rnd;
        return rtick;
    }





}